/*- * See the file LICENSE for redistribution information. * * Copyright (c) 2005-2006 * Sleepycat Software. All rights reserved. * * $Id: Sequence.java,v 1.1 2006/05/06 08:59:29 ckaestne Exp $ */ package com.sleepycat.je; import java.math.BigInteger; import java.nio.ByteBuffer; import java.util.logging.Level; import java.util.logging.Logger; import com.sleepycat.je.log.LogUtils; import com.sleepycat.je.txn.Locker; import com.sleepycat.je.txn.LockerFactory; /** * Javadoc for this public class is generated via * the doc templates in the doc_src directory. */ public class Sequence { private static final byte FLAG_INCR = ((byte) 0x1); private static final byte FLAG_WRAP = ((byte) 0x2); private static final byte FLAG_OVER = ((byte) 0x4); /* Allocation size for the record data. */ private static final int MAX_DATA_SIZE = 50; /* Version of the format for fields stored in the sequence record. */ private static final byte CURRENT_VERSION = 0; /* A sequence is a unique record in a database. */ private Database db; private DatabaseEntry key; /* Persistent fields. */ private boolean wrapAllowed; private boolean increment; private boolean overflow; private long rangeMin; private long rangeMax; private long storedValue; /* Handle-specific fields. */ private int cacheSize; private long cacheValue; private long cacheLast; private int nGets; private int nCachedGets; private TransactionConfig autoCommitConfig; private Logger logger; /* * The cache holds the range of values [cacheValue, cacheLast], which is * the same as [cacheValue, storedValue) at the time the record is written. * At store time, cacheLast is set to one before (after) storedValue. * * storedValue may be used by other Sequence handles with separate caches. * storedValue is always the next value to be returned by any handle that * runs out of cached values. */ /** * Opens a sequence handle, adding the sequence record if appropriate. */ Sequence(Database db, Transaction txn, DatabaseEntry key, SequenceConfig config) throws DatabaseException { if (db.getDatabaseImpl().getSortedDuplicates()) { throw new IllegalArgumentException ("Sequences not supported in databases configured for " + "duplicates"); } SequenceConfig useConfig = (config != null) ? config : SequenceConfig.DEFAULT; if (useConfig.getRangeMin() >= useConfig.getRangeMax()) { throw new IllegalArgumentException ("Minimum sequence value must be less than the maximum"); } if (useConfig.getInitialValue() > useConfig.getRangeMax() || useConfig.getInitialValue() < useConfig.getRangeMin()) { throw new IllegalArgumentException ("Initial sequence value is out of range"); } if (useConfig.getRangeMin() > useConfig.getRangeMax() - useConfig.getCacheSize()) { throw new IllegalArgumentException ("The cache size is larger than the sequence range"); } if (config.getAutoCommitNoSync()) { autoCommitConfig = new TransactionConfig(); autoCommitConfig.setNoSync(true); } else { /* Use the environment's default transaction config. */ autoCommitConfig = null; } this.db = db; this.key = copyEntry(key); logger = db.getEnvironment().getEnvironmentImpl().getLogger(); /* Perform an auto-commit transaction to create the sequence. */ Locker locker = null; Cursor cursor = null; OperationStatus status = OperationStatus.NOTFOUND; try { locker = LockerFactory.getWritableLocker (db.getEnvironment(), txn, db.isTransactional(), false, autoCommitConfig); cursor = new Cursor(db, locker, null); if (useConfig.getAllowCreate()) { /* Get the persistent fields from the config. */ rangeMin = useConfig.getRangeMin(); rangeMax = useConfig.getRangeMax(); increment = !useConfig.getDecrement(); wrapAllowed = useConfig.getWrap(); storedValue = useConfig.getInitialValue(); /* * To avoid dependence on SerializableIsolation, try * putNoOverwrite first. If it fails, then try to get an * existing record. */ status = cursor.putNoOverwrite(key, makeData()); if (status == OperationStatus.KEYEXIST) { if (useConfig.getExclusiveCreate()) { throw new DatabaseException ("ExclusiveCreate=true and the sequence record " + "already exists."); } if (!readData(cursor, null)) { throw new DatabaseException ("Sequence record removed during openSequence."); } status = OperationStatus.SUCCESS; } } else { /* Get an existing record. */ if (!readData(cursor, null)) { throw new DatabaseException ("AllowCreate=false and the sequence record " + "does not exist."); } status = OperationStatus.SUCCESS; } } finally { if (cursor != null) { cursor.close(); } if (locker != null) { locker.operationEnd(status); } } /* * cacheLast is initialized such that the cache will be considered * empty the first time get() is called. */ cacheSize = useConfig.getCacheSize(); cacheValue = storedValue; cacheLast = increment ? (storedValue - 1) : (storedValue + 1); } /** * Javadoc for this public method is generated via * the doc templates in the doc_src directory. */ public void close() throws DatabaseException { /* Defined only for DB compatibility and possible future use. */ } /** * Javadoc for this public method is generated via * the doc templates in the doc_src directory. * * <p>This method is synchronized to protect updating of the cached value, * since multiple threads may share a single handle. Multiple handles * for the same database/key may be used to increase concurrency.</p> */ public synchronized long get(Transaction txn, int delta) throws DatabaseException { /* Check parameters, being careful of overflow. */ if (delta <= 0) { throw new IllegalArgumentException ("Sequence delta must be greater than zero"); } if (rangeMin > rangeMax - delta) { throw new IllegalArgumentException ("Sequence delta is larger than the range"); } /* Status variables for tracing. */ boolean cached = true; boolean wrapped = false; /* * Determine whether we have exceeded the cache. The cache size is * always <= Integer.MAX_VALUE, so we don't have to worry about * overflow here as long as we subtract the two long values first. */ if ((increment && delta > ((cacheLast - cacheValue) + 1)) || (!increment && delta > ((cacheValue - cacheLast) + 1))) { cached = false; /* * We need to allocate delta or cacheSize values, whichever is * larger, by incrementing or decrementing the stored value by * adjust. */ int adjust = (delta > cacheSize) ? delta : cacheSize; /* Perform an auto-commit transaction to update the sequence. */ Locker locker = null; Cursor cursor = null; OperationStatus status = OperationStatus.NOTFOUND; try { locker = LockerFactory.getWritableLocker (db.getEnvironment(), txn, db.isTransactional(), false, autoCommitConfig); cursor = new Cursor(db, locker, null); /* Get the existing record. */ readDataRequired(cursor, LockMode.RMW); /* If we would have wrapped when not allowed, overflow. */ if (overflow) { throw new DatabaseException ("Sequence overflow " + storedValue); } /* * Handle wrapping. The range size can be larger than a long * can hold, so to avoid arithmetic overflow we use BigInteger * arithmetic. Since we are going to write, the BigInteger * overhead is acceptable. */ BigInteger availBig; if (increment) { /* Available amount: rangeMax - storedValue */ availBig = BigInteger.valueOf(rangeMax). subtract(BigInteger.valueOf(storedValue)); } else { /* Available amount: storedValue - rangeMin */ availBig = BigInteger.valueOf(storedValue). subtract(BigInteger.valueOf(rangeMin)); } if (availBig.compareTo(BigInteger.valueOf(adjust)) < 0) { /* If availBig < adjust then availBig fits in an int. */ int availInt = (int) availBig.longValue(); if (availInt < delta) { if (wrapAllowed) { /* Wrap to the opposite range end point. */ storedValue = increment ? rangeMin : rangeMax; wrapped = true; } else { /* Signal an overflow next time. */ overflow = true; adjust = 0; } } else { /* * If the delta fits in the cache available, don't wrap * just to allocate the full cacheSize; instead, * allocate as much as is available. */ adjust = availInt; } } /* Negate the adjustment for decrementing. */ if (!increment) { adjust = -adjust; } /* Set the stored value one past the cached amount. */ storedValue += adjust; /* Write the new stored value. */ cursor.put(key, makeData()); status = OperationStatus.SUCCESS; } finally { if (cursor != null) { cursor.close(); } if (locker != null) { locker.operationEnd(status); } } /* The cache now contains the range: [cacheValue, storedValue) */ cacheValue = storedValue - adjust; cacheLast = storedValue + (increment ? (-1) : 1); } /* Return the current value and increment/decrement it by delta. */ long retVal = cacheValue; if (increment) { cacheValue += delta; } else { cacheValue -= delta; } /* Increment stats. */ nGets += 1; if (cached) { nCachedGets += 1; } /* Trace this method at the FINEST level. */ if (logger.isLoggable(Level.FINEST)) { logger.log (Level.FINEST, "Sequence.get" + " value=" + retVal + " cached=" + cached + " wrapped=" + wrapped); } return retVal; } /** * Javadoc for this public method is generated via * the doc templates in the doc_src directory. */ public Database getDatabase() throws DatabaseException { return db; } /** * Javadoc for this public method is generated via * the doc templates in the doc_src directory. */ public DatabaseEntry getKey() throws DatabaseException { return copyEntry(key); } /** * Javadoc for this public method is generated via * the doc templates in the doc_src directory. */ public SequenceStats getStats(StatsConfig config) throws DatabaseException { if (config == null) { config = StatsConfig.DEFAULT; } if (!config.getFast()) { /* * storedValue may have been updated by another handle since it * was last read by this handle. Fetch the last written value. * READ_UNCOMMITTED must be used to avoid lock conflicts. */ Cursor cursor = db.openCursor(null, null); try { readDataRequired(cursor, LockMode.READ_UNCOMMITTED); } finally { cursor.close(); } } SequenceStats stats = new SequenceStats (nGets, nCachedGets, storedValue, cacheValue, cacheLast, rangeMin, rangeMax, cacheSize); if (config.getClear()) { nGets = 0; nCachedGets = 0; } return stats; } /** * Reads persistent fields from the sequence record. * Throws an exception if the key is not present in the database. */ private void readDataRequired(Cursor cursor, LockMode lockMode) throws DatabaseException { if (!readData(cursor, lockMode)) { throw new DatabaseException ("The sequence record has been deleted while it is open."); } } /** * Reads persistent fields from the sequence record. * Returns false if the key is not present in the database. */ private boolean readData(Cursor cursor, LockMode lockMode) throws DatabaseException { /* Fetch the sequence record. */ DatabaseEntry data = new DatabaseEntry(); OperationStatus status = cursor.getSearchKey(key, data, lockMode); if (status != OperationStatus.SUCCESS) { return false; } ByteBuffer buf = ByteBuffer.wrap(data.getData()); /* Get the persistent fields from the record data. */ byte ignoreVersionForNow = buf.get(); byte flags = buf.get(); rangeMin = LogUtils.readLong(buf); rangeMax = LogUtils.readLong(buf); storedValue = LogUtils.readLong(buf); increment = (flags & FLAG_INCR) != 0; wrapAllowed = (flags & FLAG_WRAP) != 0; overflow = (flags & FLAG_OVER) != 0; return true; } /** * Makes a storable database entry from the persistent fields. */ private DatabaseEntry makeData() { byte[] data = new byte[MAX_DATA_SIZE]; ByteBuffer buf = ByteBuffer.wrap(data); byte flags = 0; if (increment) { flags |= FLAG_INCR; } if (wrapAllowed) { flags |= FLAG_WRAP; } if (overflow) { flags |= FLAG_OVER; } buf.put(CURRENT_VERSION); buf.put(flags); LogUtils.writeLong(buf, rangeMin); LogUtils.writeLong(buf, rangeMax); LogUtils.writeLong(buf, storedValue); return new DatabaseEntry(data, 0, buf.position()); } /** * Returns a deep copy of the given database entry. */ private DatabaseEntry copyEntry(DatabaseEntry entry) { int len = entry.getSize(); byte[] data; if (len == 0) { data = LogUtils.ZERO_LENGTH_BYTE_ARRAY; } else { data = new byte[len]; System.arraycopy (entry.getData(), entry.getOffset(), data, 0, data.length); } return new DatabaseEntry(data); } }